<?php

namespace addons\autokeyword;

use app\admin\model\AutokeywordConfig;
use app\admin\model\AutokeywordIndex;
use app\admin\model\AutokeywordKeyword;
use app\admin\model\AutokeywordSensitive;
use app\admin\model\AutokeywordTable;
use app\common\library\Menu;
use think\Addons;
use think\Config;
use think\Db;
use think\Exception;
use think\Log;
use think\Request;
use think\Response;

/**
 * 插件
 */
class Autokeyword extends Addons
{

    private $config = [];

    private $sensitiveStopKey = false;
    private $module = 1;

    /**
     * 插件安装方法
     * @return bool
     */
    public function install()
    {
        $menu = [
            [
                'name'    => 'autokeyword',
                'title'   => '关键字自动关联(敏感字版)',
                'icon'    => 'fa fa-magic',
                'sublist' => [
                    [
                        'name'    => 'autokeyword/config',
                        'title'   => '全局配置管理',
                        'icon'    => 'fa fa-table',
                        'remark'  => '配置插件整体实现效果和方式',
                        'weigh'   => 3,
                        'sublist' => [
                            ['name' => 'autokeyword/config/index', 'title' => '查看']
                        ]
                    ],
                    [
                        'name'    => 'autokeyword/table',
                        'title'   => '后台规则管理',
                        'icon'    => 'fa fa-table',
                        'remark'  => '添加时请仔细阅读表单中蓝色文字内容且开发程序符合FastAdmin规则',
                        'weigh'   => 1,
                        'sublist' => [
                            ['name' => 'autokeyword/table/index', 'title' => '查看']
                        ]
                    ],
                    [
                        'name'    => 'autokeyword/index',
                        'title'   => '前台规则管理',
                        'icon'    => 'fa fa-table',
                        'remark'  => '添加时请仔细阅读表单中蓝色文字内容且开发程序符合FastAdmin规则',
                        'weigh'   => 2,
                        'sublist' => [
                            ['name' => 'autokeyword/index/index', 'title' => '查看']
                        ]
                    ],
                    [
                        'name'    => 'autokeyword/type',
                        'title'   => '字词分类管理',
                        'icon'    => 'fa fa-edit',
                        'remark'  => '建议采用导入方式添加关键字',
                        'weigh'   => 4,
                        'sublist' => [
                            ['name' => 'autokeyword/type/index', 'title' => '查看']
                        ]
                    ],
                    [
                        'name'    => 'autokeyword/keyword',
                        'title'   => '关键字管理',
                        'icon'    => 'fa fa-table',
                        'remark'  => '建议采用导入方式添加关键字',
                        'weigh'   => 6,
                        'sublist' => [
                            ['name' => 'autokeyword/keyword/index', 'title' => '查看']
                        ]
                    ],
                    [
                        'name'    => 'autokeyword/sensitive',
                        'title'   => '敏感字管理',
                        'icon'    => 'fa fa-table',
                        'remark'  => '建议采用导入方式添加敏感字',
                        'weigh'   => 5,
                        'sublist' => [
                            ['name' => 'autokeyword/sensitive/index', 'title' => '查看']
                        ]
                    ]
                ]
            ]
        ];

        Menu::create($menu);

        return true;
    }

    /**
     * 插件卸载方法
     * @return bool
     */
    public function uninstall()
    {
        Menu::delete('autokeyword');

        return true;
    }

    /**
     * 插件启用方法
     * @return bool
     */
    public function enable()
    {
        $this->tableConfigEdit();

        Menu::enable('autokeyword');

        return true;
    }

    private function tableConfigEdit()
    {

        $prefix = Config::get('database.prefix');

        $fiedName = Db::query("SELECT column_name FROM information_schema.columns WHERE table_name = '{$prefix}autokeyword_config' and column_name = 'position'");

        if (count($fiedName) > 0 ) {

            Db::startTrans();
            try{
                $configData = Db::name('autokeyword_config')->where('id', '>', 0)->select();

                $insertConfig = [
                    ['position'=>'0','title'=>'启用关联关键字','key'=>'autoKey','value'=>'0','description'=>'是否开启自动关联关键字','type'=>'switch','content'=>NULL],
                    ['position'=>'0','title'=>'启用敏感字检测','key'=>'sensitive','value'=>'0','description'=>'是否开启敏感字检测','type'=>'switch','content'=>NULL],
                    ['position'=>'0','title'=>'关键字打开方式','key'=>'target','value'=>'1','description'=>'关键字打开方式有当前和新窗口打开，默认是新窗口','type'=>'radio','content'=>'{"1":"新窗口","0":"当前窗口"}'],
                    ['position'=>'0','title'=>'关键字是否加粗','key'=>'bold','value'=>'0','description'=>'默认为不启用加粗','type'=>'radio','content'=>'{"1":"加粗","0":"正常"}'],
                    ['position'=>'0','title'=>'关键字是否下划线','key'=>'underscore','value'=>'0','description'=>'默认不启用下划线','type'=>'radio','content'=>'{"1":"下划线","0":"正常"}' ],
                    ['position'=>'0','title'=>'关键字颜色','key'=>'color','value'=>'#1e96eb','description'=>'默认不设置，跟随主题css设置颜色','type'=>'picker','content'=>NULL],
                    ['position'=>'0','title'=>'文章内关键字数量','key'=>'keysum','value'=>'5','description'=>'设置文章内关键字的最大数量','type'=>'number','content'=>NULL],
                    ['position'=>'0','title'=>'单一关键字数量','key'=>'onlysum','value'=>'2','description'=>'一个词在文章内最多允许关联几次(文章内关键字数量权限高于该配置项）','type'=>'number','content'=>NULL],
                    ['position'=>'0','title'=>'启用关键字自定义HTML','key'=>'html','value'=>'0','description'=>'是否启用自定义的关键字样式','type'=>'switch','content'=>NULL],
                    ['position'=>'0','title'=>'自定义HTML','key'=>'keyhtml','value'=>'<p><a class="newclass" href="{url}">{title}</a></p>','description'=>'{url} 为关键字链接地址{title}为关键字','type'=>'editor','content'=>NULL],
                    ['position'=>'0','title'=>'敏感字替换标识','key'=>'sensitivemark','value'=>'*','description'=>'检测到的敏感字用何种字符替换，默认“ * ”','type'=>'string','content'=>NULL],
                    ['position'=>'0','title'=>'敏感字是否阻拦','key'=>'sensitiveStop','value'=>'1','description'=>'是则阻拦后将禁止提交内容并返回敏感字提示信息，否则替换敏感字继续提交','type'=>'switch','content'=>NULL],
                    ['position'=>'0','title'=>'敏感字阻拦信息','key'=>'sensitiveText','value'=>'发布内容含有敏感字请重新编辑','description'=>'阻拦敏感字时返回的信息，采用$this->error(\'错误信息\')','type'=>'string','content'=>NULL],
                    ['position'=>'1','title'=>'启用关联关键字','key'=>'autoKey','value'=>'0','description'=>'是否开启自动关联关键字','type'=>'switch','content'=>NULL],
                    ['position'=>'1','title'=>'启用敏感字检测','key'=>'sensitive','value'=>'0','description'=>'是否开启敏感字检测','type'=>'switch','content'=>NULL],
                    ['position'=>'1','title'=>'关键字打开方式','key'=>'target','value'=>'1','description'=>'关键字打开方式有当前和新窗口打开，默认是新窗口，','type'=>'radio','content'=>'{"1":"新窗口","0":"当前窗口"}' ],
                    ['position'=>'1','title'=>'关键字是否加粗','key'=>'bold','value'=>'0','description'=>'默认为不启用加粗','type'=>'radio','content'=>'{"1":"加粗","0":"正常"}' ],
                    ['position'=>'1','title'=>'关键字是否下划线','key'=>'underscore','value'=>'0','description'=>'默认不启用下划线','type'=>'radio','content'=>'{"1":"下划线","0":"正常"}' ],
                    ['position'=>'1','title'=>'关键字颜色','key'=>'color','value'=>'#1e96eb','description'=>'默认不设置，跟随主题css设置颜色','type'=>'picker','content'=>NULL],
                    ['position'=>'1','title'=>'文章内关键字数量','key'=>'keysum','value'=>'5','description'=>'设置文章内关键字的最大数量','type'=>'number','content'=>NULL],
                    ['position'=>'1','title'=>'单一关键字数量','key'=>'onlysum','value'=>'2','description'=>'一个词在文章内最多允许关联几次(文章内关键字数量权限高于该配置项）','type'=>'number','content'=>NULL],
                    ['position'=>'1','title'=>'启用关键字自定义HTML','key'=>'html','value'=>'0','description'=>'是否启用自定义的关键字样式','type'=>'switch','content'=>NULL],
                    ['position'=>'1','title'=>'自定义HTML','key'=>'keyhtml','value'=>'<p><a class="newclass" href="{url}">{title}</a></p>','description'=>'{url} 为关键字链接地址{title}为关键字','type'=>'editor','content'=>NULL],
                    ['position'=>'1','title'=>'敏感字替换标识','key'=>'sensitivemark','value'=>'*','description'=>'检测到的敏感字用何种字符替换，默认“ * ”','type'=>'string','content'=>NULL],
                    ['position'=>'1','title'=>'敏感字是否阻拦','key'=>'sensitiveStop','value'=>'1','description'=>'是则阻拦后将禁止提交内容并返回敏感字提示信息，否则替换敏感字继续提交','type'=>'switch','content'=>NULL],
                    ['position'=>'1','title'=>'敏感字阻拦信息','key'=>'sensitiveText','value'=>'发布内容含有敏感字请重新编辑','description'=>'阻拦敏感字时返回的信息，采用$this->error(\'错误信息\')','type'=>'string','content'=>NULL]
                ];

                foreach ($configData as $key => $value) {
                    $insertConfig[$key] = array_merge($insertConfig[$key], $value);
                    unset($insertConfig[$key]['id']);
                }

                $dataQuantity = Db::name('autokeyword_config')->insertAll($insertConfig);

                if (count($fiedName) > 0 && $dataQuantity >= 26) {
                    Db::query("TRUNCATE TABLE {$prefix}autokeyword_config");
                    Db::name('autokeyword_config')->insertAll($insertConfig);
                } else {
                    Db::rollback();
                    return $this->error("全局配置表 {$prefix}autokeyword_config 的数据更新失败，请根据以有的配置数据和插件中的install.sql文件中针对 {$prefix}autokeyword_config 表的插入SQL语句进行手动更新添加或者联系插件作者解决问题！");
                }

                Db::commit();
            } catch (\PDOException $e) {
                Db::rollback();
            }
        }
    }

    /**
     * 插件禁用方法
     * @return bool
     */
    public function disable()
    {
        Menu::disable('autokeyword');

        return true;
    }

    public function actionBegin($call)
    {

        try {
            // 获取配置信息
            $configDb = new AutokeywordConfig();

            // 闭包获取相关私有属性
            $closure = function () {
                return [
                    'this'    => $this,
                    'model'   => $this->model ?? false,
                    'request' => $this->request ?? false,
                ];
            };

            $callArray = $closure->call($call[0]);

            // 判断是否有请求过来
            if (!$callArray['request']) {
                return $call;
            }

            // 提交方式检测，GET跳过
            if (!$callArray['request']->isPost() && !$callArray['request']->isAjax()) {
                return $call;
            }

            // 是否是后端提交默认前端
            if ($callArray['model']) {
                $this->module = 0;
            }

            try {

                // 获取配置值
                foreach ($configDb->field('key,value')->where('position', '=', $this->module)->select() as $value) {
                    $this->config[$value['key']] = $value['value'];
                }

                // 检测是否开启
                if (!isset($this->config['autoKey']) || !$this->config['autoKey'] && !$this->config['sensitive']) {
                    return $call;
                }
            } catch (Exception $e) {
                return $call;
            }

            if ($this->module === 0) {
                $data = $this->goToAdmin($callArray, $call);
                return $data;
            }

            if ($this->module === 1) {
                $data = $this->goToIndex($callArray, $call);
                return $data;
            }
        } catch (\Error $e) {
            return $call;
        }

        return $call;
    }

    /**
     * 前端检测
     *
     * @param $callArray
     * @param $call
     *
     * @return mixed
     * @throws \think\exception\DbException
     */
    private function goToIndex($callArray, $call)
    {
        $request = $callArray['request'];

        $dispatch = $request->dispatch();

        list($module, $controller, $action) =  $dispatch['module'];

        // 获取设置信息
        $ruleData = AutokeywordIndex::all();

        // 生效的规则字段数据
        $ruleFieldData = [];

        // 获取请求数据
        $requestData = $request->param();

        foreach ($ruleData as $value) {
            if (
                $value->getData('module') !== $module ||
                strtolower($value->getData('controller')) !== strtolower($controller) ||
                strtolower($value->getData('action')) !== strtolower($action)
            ) {
                continue;
            }

            $field = explode(',', $value->getData('field'));

            $ruleFieldData = array_merge($ruleFieldData, array_intersect_key($requestData, array_flip($field)));

        }
        // 判断是否符合信息
        if (count($ruleFieldData) <= 0) {
            return $call;
        }

        foreach ($ruleFieldData as $key => $value) {

            try {
                // 替换及敏感字筛查
                $string = $this->substitute($value);
            } catch (Exception $exception) {
                $string = $value;
                Log::write('关键字关联插件替换或筛查时发生异常');
            }

            // 置换数据
            $requestData[$key] = $string;
        }

        // 设置置换后的数据
        $callArray['request']->get($requestData);
        $callArray['request']->put($requestData);
        $callArray['request']->post($requestData);
        $callArray['request']->request($requestData);

        $closure = function () use ($callArray) {
            $this->request = $callArray['request'];
            return $this;
        };

        $call[0] = $closure->call($call[0]);

        return $call;
    }

    /**
     * 后端检测
     * @param $callArray
     * @param $call
     *
     * @return mixed
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\ModelNotFoundException
     * @throws \think\exception\DbException
     */
    private function goToAdmin($callArray, $call)
    {

        try {
            $ref = new \ReflectionClass($callArray['model']);

            if (!$ref->hasProperty('name')) {
                return $call;
            }

            // 获取属性ReflectionProperty对象
            $model = $ref->getProperty('name');
            // 设置可访问
            $model->setAccessible(true);
            // 获取属性值
            $tableName = $model->getValue($callArray['model']);

            if (!$tableName) {
                return $call;
            }

            // 获取用户选择生效的数据表及字段和方法
            $tableDB  = new AutokeywordTable();
            $tableAll = $tableDB->select();

            $prefix = Config::get('database.prefix');

            $tableArray = [];

            foreach ($tableAll as $key => $value) {

                if ($value['table'] === $prefix . $tableName) {
                    $tableArray[$tableName][$value['field']] = explode(',', $value['action']);

                    $tableArray[$tableName][$value['field']]['mark'] = $value['mark'];
                }
            }

            // 判断是否为需要关联关键字的数据表
            if (isset($tableArray[$tableName])) {

                $action = $callArray['request']->action();
                // 可用字段
                $fields = [];
                // 获取表可用字段
                array_walk($tableArray[$tableName], function ($actionAry, $field, $action) use (&$fields) {

                    $keys = array_search($action, $actionAry);

                    if (false !== $keys) {
                        $fields[$field] = $actionAry['mark'] ?? 0;
                    }

                }, $action);

                if (count($fields) > 0) {

                    $param = $this->getParam($callArray['request'], $fields);

                    $data = $callArray['request']->param();;

                    foreach ($fields as $key => $mark) {

                        if (isset($param[$key]) && is_string($param[$key])) {

                            try {
                                // 替换及敏感字筛查
                                $string = $this->substitute($param[$key]);
                            } catch (Exception $exception) {
                                $string = $param[$key];
                                Log::write('关键字关联插件替换或筛查时发生异常');
                            }

                            if (intval($mark) === 1) {
                                $data[$key] = $string;
                            } else {
                                $data['row'][$key] = $string;
                            }
                        }
                    }

                    $method = $callArray['request']->method(true);

                    // 自动获取请求变量
                    switch ($method) {
                        case 'POST':
                            $callArray['request']->post($data);
                            break;
                        case 'PUT':
                            $callArray['request']->put($data);
                            break;
                    }

                    $closure = function () use ($callArray) {
                        $this->request = $callArray['request'];
                        return $this;
                    };

                    $call[0] = $closure->call($call[0]);

                    return $call;
                }
            }

        } catch (\ReflectionException $e) {
            return $call;
        }

        return $call;
    }

    /**
     * 检查关键字和敏感字
     *
     * @param string $param
     *
     * @return null|string|string[]
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\ModelNotFoundException
     * @throws \think\exception\DbException
     */
    private function substitute(string $param)
    {

        // 检测敏感字
        if ($this->config['sensitive']) {

            $sensitiveDB = new AutokeywordSensitive();

            // 获取敏感字数据条件
            $where['module'] = ['<>', 1];
            if (!$this->module) {
                $where['module'] = ['<>', 0];
            }

            // 获取敏感字数据
            $sensitiveAll = $sensitiveDB->field('title')->where($where)->select();

            foreach ($sensitiveAll as $value) {
                $param = preg_replace_callback("/{$value['title']}/", function ($a) {

                    $len = mb_strlen($a[0]);
                    $str = '';

                    if (intval($len) > 0) {
                        $this->sensitiveStopKey = true;
                    }

                    while (intval($len) > 0) {
                        $len--;
                        $str .= $this->config['sensitivemark'];
                    }

                    return $str;
                }, $param);

            }

            if ($this->sensitiveStopKey) {
                if (intval($this->config['sensitiveStop']) === 1) {
                    $this->error($this->config['sensitiveText']);
                }
            };

        }

        // 检测关键字
        if ($this->config['autoKey']) {
            $keywordDb = new AutokeywordKeyword();

            // 获取自定义关键字数据
            $keywordAll = $keywordDb->field('title, url')->where($where)->select();

            // 判断是否启用自定义HTML
            if (!$this->config['html']) {
                // 设置a标签样式
                $target = '_blank';
                if (!$this->config['target']) {
                    $target = '_self';
                }

                $bold = '';
                if ($this->config['bold']) {
                    $bold = 'font-weight: bold !important;';
                }

                $underscore = '';
                if ($this->config['underscore']) {
                    $underscore = 'text-decoration: underline !important;';
                }

                $color = '';
                if ($this->config['color']) {
                    $color = "color:{$this->config['color']} !important;";
                }
            }

            $sum = 0;

            // 缺少内容表的标题匹配链接
            foreach ($keywordAll as $value) {

                // 数量判断
                if (($this->config['keysum'] - $sum) < $this->config['onlysum']) {
                    $this->config['onlysum'] = ($this->config['keysum'] - $sum);
                }
                // 匹配是否已经替换过
                $pregSum = preg_match_all("/<a[^>]+href=\"(([^\"]+)\")>{$value['title']}<\/a>/i", $param);

                // 判断已经替换过的关键字数量
                if ($pregSum >= intval($this->config['onlysum'])) {
                    $sum = $sum + $pregSum;
                    continue;
                }

                // 判断是否超过数量限制
                if ($sum >= $this->config['keysum']) {
                    break;
                }
                // 定义替换的a链接html
                if (!$this->config['html']) {
                    $html = "<a style='{$underscore}{$bold}{$color}' target='{$target}' href='{$value['url']}'>{$value['title']}</a>";
                } else {
                    $html = preg_replace("/{url}/", $value['url'], $this->config['keyhtml']);
                    $html = preg_replace("/{title}/", $value['title'], $html);
                }
                // 去除存在的关键字a链接
                $param = preg_replace("/<a[^>]+href=\"(([^\"]+)\")>{$value['title']}<\/a>/i", $value['title'], $param);
                // 替换
                $param = preg_replace("/{$value['title']}/i", $html, $param, $this->config['onlysum'], $count);

                $sum = $sum + $count;
            }
        }

        return $param;
    }

    /**
     * 返回表单数据
     *
     * @param Request $request
     * @param array   $fields
     *
     * @return bool|mixed
     */
    private function getParam(Request $request, array $fields = [])
    {
        $param = false;

        // 获取非数组形式的值
        foreach ($fields as $key => $value) {
            if (intval($value) === 1) {
                $param[$key] = $request->param($key) ?? false;
            } else {
                $param[$key] = $request->param("row.{$key}") ?? false;
            }
        }

        return $param;
    }

    /**
     * 返回
     *
     * @param string $msg
     * @param null   $url
     * @param string $data
     * @param int    $wait
     * @param array  $header
     */
    protected function error($msg = '', $url = null, $data = '', $wait = 3, array $header = [])
    {
        if (is_null($url)) {
            $url = Request::instance()->isAjax() ? '' : 'javascript:history.back(-1);';
        } elseif ('' !== $url && !strpos($url, '://') && 0 !== strpos($url, '/')) {
            $url = \think\Url::build($url);
        }

        $type = Request::instance()->isAjax() ? Config::get('default_ajax_return') : Config::get('default_return_type');

        $result = [
            'code' => 0,
            'msg'  => $msg,
            'data' => $data,
            'url'  => $url,
            'wait' => $wait,
        ];

        if ('html' == strtolower($type)) {
            $template = Config::get('template');
            $view = Config::get('view_replace_str');

            $result = \think\View::instance($template, $view)
                ->fetch(Config::get('dispatch_error_tmpl'), $result);
        }

        $response = Response::create($result, $type)->header($header);

        throw new \think\exception\HttpResponseException($response);
    }

}
